/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.util;



import java.util.Arrays;

import java.util.Objects;



/**

 * An exception that was thrown in user-code. Sets the stack trace from the first time execution

 * enters user code down through the rest of the user's stack frames until the exception is reached.

 */

public class UserCodeException extends RuntimeException {



  public static UserCodeException wrap(Throwable t) {

    if (t instanceof UserCodeException) {

      return (UserCodeException) t;

    }



    return new UserCodeException(t);

  }



  public static RuntimeException wrapIf(boolean condition, Throwable t) {

    if (condition) {

      return wrap(t);

    }



    if (t instanceof RuntimeException) {

      return (RuntimeException) t;

    }



    return new RuntimeException(t);

  }



  private UserCodeException(Throwable t) {

    super(t);

    truncateStackTrace(t);

  }



  /**

   * Truncates the @{Throwable}'s stack trace to contain only user code, removing all frames below.

   *

   * <p>This is to remove infrastructure noise below user code entry point. We do this by finding

   * common stack frames between the throwable's captured stack and that of the current thread.

   */

  private void truncateStackTrace(Throwable t) {

    StackTraceElement[] currentStack = Thread.currentThread().getStackTrace();

    StackTraceElement[] throwableStack = t.getStackTrace();



    int currentStackSize = currentStack.length;

    int throwableStackSize = throwableStack.length;



    if (throwableStackSize == 0) {

      // Nothing to truncate.

      return;

    }



    int commonFrames = 0;

    while (framesEqual(

        currentStack[currentStackSize - commonFrames - 1],

        throwableStack[throwableStackSize - commonFrames - 1])) {

      commonFrames++;

      if (commonFrames >= Math.min(currentStackSize, throwableStackSize)) {

        break;

      }

    }



    StackTraceElement[] truncatedStack =

        Arrays.copyOfRange(throwableStack, 0, throwableStackSize - commonFrames);

    t.setStackTrace(truncatedStack);

  }



  /**

   * Check if two frames are equal; Frames are considered equal if they point to the same method.

   */

  private boolean framesEqual(StackTraceElement frame1, StackTraceElement frame2) {

    boolean areEqual = Objects.equals(frame1.getClassName(), frame2.getClassName());

    areEqual &= Objects.equals(frame1.getMethodName(), frame2.getMethodName());



    return areEqual;

  }

}